﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel.Activation;
using System.Web;
using SL.WcfExceptionHandling;
using IssueVision.EntityModel;
using IssueVision.Helper;
using IssueVision.Resource;

namespace IssueVision.Service
{
    [SilverlightFaultBehavior]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    public class IssueVisionService : IIssueVisionService
    {
        /// <summary>
        /// Get a list of all IssueTypes
        /// </summary>
        /// <returns></returns>
        public List<IssueType> GetIssueTypes()
        {
            using (IssueVisionEntities context = new IssueVisionEntities())
            {
                return context.IssueTypes.ToList();
            }
        }

        /// <summary>
        /// Get a list of all Platforms
        /// </summary>
        /// <returns></returns>
        public List<Platform> GetPlatforms()
        {
            using (IssueVisionEntities context = new IssueVisionEntities())
            {
                return context.Platforms.ToList();
            }
        }

        /// <summary>
        /// Get a list of all Resolutions
        /// </summary>
        /// <returns></returns>
        public List<Resolution> GetResolutions()
        {
            using (IssueVisionEntities context = new IssueVisionEntities())
            {
                return context.Resolutions.ToList();
            }
        }

        /// <summary>
        /// Get a list of all SecurityQuestions
        /// </summary>
        /// <returns></returns>
        public List<SecurityQuestion> GetSecurityQuestions()
        {
            using (IssueVisionEntities context = new IssueVisionEntities())
            {
                return context.SecurityQuestions.ToList();
            }
        }

        /// <summary>
        /// Get a list of all Statuses
        /// </summary>
        /// <returns></returns>
        public List<Status> GetStatuses()
        {
            using (IssueVisionEntities context = new IssueVisionEntities())
            {
                return context.Statuses.ToList();
            }
        }

        /// <summary>
        /// Get a list of all SubStatuses
        /// </summary>
        /// <returns></returns>
        public List<SubStatus> GetSubStatuses()
        {
            using (IssueVisionEntities context = new IssueVisionEntities())
            {
                return context.SubStatuses.ToList();
            }
        }

        /// <summary>
        /// Get a list of all Users
        /// </summary>
        /// <returns></returns>
        public List<User> GetUsers()
        {
            using (IssueVisionEntities context = new IssueVisionEntities())
            {
                return context.Users.ToList();
            }
        }

        /// <summary>
        /// Get current user
        /// </summary>
        /// <returns></returns>
        public User GetCurrentUser()
        {
            using (IssueVisionEntities context = new IssueVisionEntities())
            {
                User foundUser = context.Users.FirstOrDefault(n =>
                    n.Name == HttpContext.Current.User.Identity.Name);

                if (foundUser != null)
                {
                    return foundUser;
                }
                throw new UnauthorizedAccessException(ErrorResources.NoUserFound);
            }
        }

        /// <summary>
        /// Returns True if the ProfileReset flag is set
        /// for current user; otherwise, False
        /// </summary>
        /// <returns></returns>
        public bool GetCurrentUserProfileReset()
        {
            using (IssueVisionEntities context = new IssueVisionEntities())
            {
                User foundUser = context.Users.FirstOrDefault(n => 
                    n.Name == HttpContext.Current.User.Identity.Name);

                if (foundUser != null)
                {
                    return (foundUser.Settings.ProfileReset != 0);
                }
                throw new UnauthorizedAccessException(ErrorResources.NoUserFound);
            }
        }

        /// <summary>
        /// Get all issues orderby by StatusID, and Priority
        /// </summary>
        /// <returns></returns>
        public List<Issue> GetAllIssues()
        {
            using (IssueVisionEntities context = new IssueVisionEntities())
            {
                return context.Issues
                    .Include("Files")
                    .Include("Attributes")
                    .Include("Platform")
                    .OrderBy(g => g.StatusID).ThenBy(g => g.Priority)
                    .ToList();
            }
        }

        /// <summary>
        /// Get all issues assigned to the current user or
        /// opened by the current user and still not assigned
        /// any one.  The list is ordered by StatusID, and Priority
        /// </summary>
        /// <returns></returns>
        public List<Issue> GetMyIssues()
        {
            using (IssueVisionEntities context = new IssueVisionEntities())
            {
                return context.Issues
                    .Include("Files")
                    .Include("Attributes")
                    .Include("Platform")
                    .Where(g => (g.AssignedToID.Equals(HttpContext.Current.User.Identity.Name)
                    || (g.AssignedToID == null &&
                        g.OpenedByID.Equals(HttpContext.Current.User.Identity.Name))))
                    .OrderBy(g => g.StatusID).ThenBy(g => g.Priority)
                    .ToList();
            }
        }

        /// <summary>
        /// Get all un-resolved issues
        /// </summary>
        /// <returns></returns>
        public List<Issue> GetAllUnResolvedIssues()
        {
            using (IssueVisionEntities context = new IssueVisionEntities())
            {
                return context.Issues
                    .Where(n => (n.ResolutionID == null || n.ResolutionID == 0))
                    .ToList();
            }
        }

        private class ServerBugCount
        {
            public int Month { get; set; }
            public int Year { get; set; }
            public int Count { get; set; }
        }

        /// <summary>
        /// Return the active bug count for the last numberOfMonth months
        /// </summary>
        /// <param name="numberOfMonth"></param>
        /// <returns></returns>
        public List<string> GetActiveBugCountByMonth(int numberOfMonth)
        {
            using (IssueVisionEntities context = new IssueVisionEntities())
            {
                var issueCountsByMonth = from n in context.Issues
                                         where (n.ResolutionID == null || n.ResolutionID == 0)
                                         group n by new { month = n.OpenedDate.Month, year = n.OpenedDate.Year } into d
                                         select new ServerBugCount
                                         {
                                             Month = d.Key.month,
                                             Year = d.Key.year,
                                             Count = d.Count()
                                         };

                List<string> resultList = new List<string>();
                // loop through issueCountsByMonth for the last numberOfMonth
                for (int i = numberOfMonth; i >= 1; i--)
                {
                    DateTime dt = DateTime.Today.AddMonths(-i);
                    // search issueCountsByMonth for the issue counts of a specific month
                    ServerBugCount currentIssueCount = issueCountsByMonth
                        .Where(n => (n.Month == dt.Month && n.Year == dt.Year))
                        .FirstOrDefault();

                    if (currentIssueCount == null)
                    {
                        // the active issue count for that month is zero
                        resultList.Add(dt.ToString("MM/yyyy") + "/0");
                    }
                    else
                    {
                        // the active issue count for that month is not zero
                        resultList.Add(currentIssueCount.Month.ToString("00") + "/" + currentIssueCount.Year + "/" + currentIssueCount.Count);
                    }
                }
                return resultList;
            }
        }

        /// <summary>
        /// Return the resolved bug count for the last numberOfMonth months
        /// </summary>
        /// <param name="numberOfMonth"></param>
        /// <returns></returns>
        public List<string> GetResolvedBugCountByMonth(int numberOfMonth)
        {
            using (IssueVisionEntities context = new IssueVisionEntities())
            {
                var issueCountsByMonth = from n in context.Issues
                                         where (n.ResolutionID != null && n.ResolutionID != 0)
                                         group n by new
                                         {
                                             month = (n.ResolutionDate ?? DateTime.Now).Month,
                                             year = (n.ResolutionDate ?? DateTime.Now).Year
                                         } into d
                                         select new ServerBugCount
                                         {
                                             Month = d.Key.month,
                                             Year = d.Key.year,
                                             Count = d.Count()
                                         };

                List<string> resultList = new List<string>();
                // loop through issueCountsByMonth for the last numberOfMonth
                for (int i = numberOfMonth; i >= 1; i--)
                {
                    DateTime dt = DateTime.Today.AddMonths(-i);
                    // search issueCountsByMonth for the issue counts of a specific month
                    ServerBugCount currentIssueCount = issueCountsByMonth
                        .Where(n => (n.Month == dt.Month && n.Year == dt.Year))
                        .FirstOrDefault();

                    if (currentIssueCount == null)
                    {
                        // the resolved issue count for that month is zero
                        resultList.Add(dt.ToString("MM/yyyy") + "/0");
                    }
                    else
                    {
                        // the resolved issue count for that month is not zero
                        resultList.Add(currentIssueCount.Month.ToString("00") + "/" + currentIssueCount.Year + "/" + currentIssueCount.Count);
                    }
                }
                return resultList;
            }
        }

        private class ServerBugCountByPriority
        {
            public int Priority { get; set; }
            public int Count { get; set; }
        }

        /// <summary>
        /// Return the active bug count by priority
        /// </summary>
        /// <returns></returns>
        public List<string> GetActiveBugCountByPriority()
        {
            using (IssueVisionEntities context = new IssueVisionEntities())
            {
                var issueCountsByPriority = from n in context.Issues
                                            where (n.ResolutionID == null || n.ResolutionID == 0)
                                            group n by new { priority = n.Priority } into d
                                            select new ServerBugCountByPriority
                                            {
                                                Priority = d.Key.priority,
                                                Count = d.Count()
                                            };

                List<string> resultList = new List<string>();
                // loop through issueCountsByPriority
                for (byte i = IssueVisionServiceConstant.HighestPriority; i <= IssueVisionServiceConstant.LowestPriority; i++)
                {
                    // search issueCountsByPriority for the issue counts of a specific priority
                    byte priority = i;
                    ServerBugCountByPriority currentIssueCount = issueCountsByPriority
                        .Where(n => (n.Priority == priority))
                        .FirstOrDefault();

                    if (currentIssueCount == null)
                    {
                        // the active issue count for that priority is zero
                        resultList.Add(i + "/0");
                    }
                    else
                    {
                        // the active issue count for that priority is not zero
                        resultList.Add(currentIssueCount.Priority + "/" + currentIssueCount.Count);
                    }
                }
                return resultList;
            }
        }

        /// <summary>
        /// Get a list of IssueHistory records for a specific IssueID
        /// </summary>
        /// <param name="issueId"></param>
        /// <returns></returns>
        public List<IssueHistory> GetIssueHistoryByIssueID(long issueId)
        {
            using (IssueVisionEntities context = new IssueVisionEntities())
            {
                return context.IssueHistories
                    .Include("Platform")
                    .Include("Status")
                    .Where(n => n.IssueID == issueId)
                    .OrderBy(n => n.DateCreated)
                    .ToList();
            }
        }

        /// <summary>
        /// When a new issue is created, we should check the following:
        /// 
        /// 1) OpenedDate, OpenedByID, LastChange, changedByID, and IssueID
        ///    should be set with their initial values.
        /// 2) If status is Open, the AssignToID should be null.
        /// 3) ResolutionDate and ResolvedByID should be set based on ResolutionID.
        /// 
        /// When a new issue is updated, we should check the following:
        /// 
        /// 1) LastChange and changedByID should be upated.
        /// 2) If status is Open, the AssignToID should be null.
        /// 3) ResolutionDate and ResolvedByID should be set based on ResolutionID.
        /// </summary>
        /// <param name="issue"></param>
        /// <returns>any warning message and the IssueID of the updated issue</returns>
        public List<object> UpdateIssue(Issue issue)
        {
            List<object> returnList = new List<object>();

            using (IssueVisionEntities context = new IssueVisionEntities())
            {
                switch (issue.ChangeTracker.State)
                {
                    case ObjectState.Added:
                        {
                            // this is inserting a new issue
                            // repeat the client-side validation on the server side first
                            issue.Validate();

                            issue.OpenedDate = DateTime.Now;
                            issue.OpenedByID = HttpContext.Current.User.Identity.Name;
                            issue.LastChange = DateTime.Now;
                            issue.ChangedByID = HttpContext.Current.User.Identity.Name;
                            long newIssueId = context.Issues.Count() > 0
                                                  ? (from iss in context.Issues select iss.IssueID).Max() + 1
                                                  : 1;
                            long newIssueHistoryId = context.IssueHistories.Count() > 0
                                                         ? (from iss in context.IssueHistories select iss.IssueID).Max() +
                                                           1
                                                         : 1;
                            // create a new Issue ID based on Issues and IssueHistories tables
                            issue.IssueID = newIssueHistoryId > newIssueId ? newIssueHistoryId : newIssueId;
                            // if status is Open, AssignedToID should be null
                            if (issue.StatusID == IssueVisionServiceConstant.OpenStatusID)
                            {
                                issue.AssignedToID = null;
                            }
                            // set ResolutionDate and ResolvedByID based on ResolutionID
                            if (issue.ResolutionID == null || issue.ResolutionID == 0)
                            {
                                issue.ResolutionDate = null;
                                issue.ResolvedByID = null;
                            }
                            else
                            {
                                issue.ResolutionDate = DateTime.Now;
                                issue.ResolvedByID = HttpContext.Current.User.Identity.Name;
                            }

                            // saving changes
                            context.Issues.ApplyChanges(issue);
                            context.SaveChanges();
                            // return the new IssueID created
                            returnList.Add(string.Empty);
                            returnList.Add(issue.IssueID);
                            return returnList;
                        }
                    case ObjectState.Deleted:
                        if (issue.StatusID == IssueVisionServiceConstant.ActiveStatusID)
                        {
                            // cannot delete an active issue
                            returnList.Add(ErrorResources.IssueWithActiveStatusID);
                            returnList.Add(0);
                            return returnList;
                        }
                        if (!HttpContext.Current.User.IsInRole(IssueVisionServiceConstant.UserTypeAdmin) &&
                            !(HttpContext.Current.User.Identity.Name.Equals(issue.AssignedToID)) &&
                            !(issue.AssignedToID == null &&
                              HttpContext.Current.User.Identity.Name.Equals(issue.OpenedByID)))
                        {
                            // no permission to delete this issue
                            returnList.Add(ErrorResources.NoPermissionToDeleteIssue);
                            returnList.Add(0);
                            return returnList;
                        }
                        context.Issues.ApplyChanges(issue);
                        context.SaveChanges();
                        returnList.Add(string.Empty);
                        returnList.Add(0);
                        return returnList;
                    default:
                        // this is updating an issue and its navigation properties
                        // repeat the client-side validation on the server side first
                        issue.Validate();
                        // retrieve the original values
                        Issue originalIssue;
                        using (IssueVisionEntities otherContext = new IssueVisionEntities())
                        {
                            originalIssue = otherContext.Issues.First(n => n.IssueID == issue.IssueID);
                        }
                        // Business logic:
                        // Admin user can read/update any issue, and
                        // normal user can only read/update issues assigned to them
                        // or issues created by them and have not assigned to anyone.
                        if (!IssueIsReadOnly(originalIssue))
                        {
                            issue.LastChange = DateTime.Now;
                            issue.ChangedByID = HttpContext.Current.User.Identity.Name;

                            // saving changes
                            context.Issues.ApplyChanges(issue);
                            context.SaveChanges();
                            // return the IssueID of the updated issue
                            returnList.Add(string.Empty);
                            returnList.Add(issue.IssueID);
                            return returnList;
                        }
                        returnList.Add(ErrorResources.NoPermissionToUpdateIssue);
                        returnList.Add(0);
                        return returnList;
                }
            }
        }

        /// <summary>
        /// Add/Update/Delete a user
        /// </summary>
        /// <param name="user"></param>
        /// <returns>any warning message</returns>
        public string UpdateUser(User user)
        {
            using (IssueVisionEntities context = new IssueVisionEntities())
            {
                switch (user.ChangeTracker.State)
                {
                    case ObjectState.Added:
                        user.Validate();
                        if (CheckUserInsertPermission(user) && (user.Settings.IsUserMaintenance != 0))
                        {
                            // Re-generate password hash and password salt
                            string currentPasswordSalt = HashHelper.CreateRandomSalt();
                            string currentPasswordHash = HashHelper.ComputeSaltedHash(user.NewPassword, currentPasswordSalt);

                            // set a valid PasswordQuestion
                            SecurityQuestion securityQuestion = context.SecurityQuestions.FirstOrDefault();
                            if (securityQuestion != null)
                                user.PasswordQuestion = securityQuestion.PasswordQuestion;

                            // requires the user to reset profile
                            user.Settings.ProfileReset = 1;

                            // saving changes
                            context.Users.ApplyChanges(user);
                            context.SaveChanges();
                            context.UpdatePasswordHashAndSalt(user.Name, 
                                                              currentPasswordHash, currentPasswordSalt);
                        }
                        else
                        {
                            return ErrorResources.NoPermissionToInsertUser;
                        }
                        break;
                    case ObjectState.Deleted:
                        if (CheckUserDeletePermission(user))
                        {
                            // server-side only validation logic
                            // check whether there is any issue still assigned to this user
                            var foundIssues = context.Issues
                                .Where(n => n.AssignedToID == user.Name);
                            if (foundIssues.Count() != 0)
                            {
                                return ErrorResources.IssueWithAssignedToID;
                            }
                            // check whether there is any issue still changed by this user
                            foundIssues = context.Issues
                                .Where(n => n.ChangedByID == user.Name);
                            if (foundIssues.Count() != 0)
                            {
                                return ErrorResources.IssueWithChangedByID;
                            }
                            // check whether there is any issue still opened by this user
                            foundIssues = context.Issues
                                .Where(n => n.OpenedByID == user.Name);
                            if (foundIssues.Count() != 0)
                            {
                                return ErrorResources.IssueWithOpenedByID;
                            }
                            // check whether there is any issue still resolved by this user
                            foundIssues = context.Issues
                                .Where(n => n.ResolvedByID == user.Name);
                            if (foundIssues.Count() != 0)
                            {
                                return ErrorResources.IssueWithResolvedByID;
                            }

                            // saving changes
                            context.Users.ApplyChanges(user);
                            context.SaveChanges();
                        }
                        else
                        {
                            return ErrorResources.NoPermissionToDeleteUser;
                        }
                        break;
                    default:
                        user.Validate();
                        if (user.Settings.IsUserMaintenance != 0)
                        {
                            // the call is from UserMaintenance screen
                            if (user.Name == HttpContext.Current.User.Identity.Name)
                            {   // the caller is updateing itself
                                // Search user from database by name
                                User foundUser = context.Users.FirstOrDefault(u => u.Name == user.Name);
                                if (foundUser != null)
                                {
                                    // verify whether the caller is admin
                                    if (IsAdminUser(foundUser))
                                    {
                                        // Re-generate password hash and password salt
                                        string currentPasswordSalt = HashHelper.CreateRandomSalt();
                                        string currentPasswordHash = HashHelper.ComputeSaltedHash(user.NewPassword, currentPasswordSalt);

                                        foundUser.FullName.FirstName = user.FullName.FirstName;
                                        foundUser.FullName.LastName = user.FullName.LastName;
                                        foundUser.Email = user.Email;
                                        foundUser.Settings.UserType = user.Settings.UserType;
                                        // requires the foundUser to reset profile
                                        foundUser.Settings.ProfileReset = 1;

                                        // saving changes
                                        context.Users.ApplyChanges(foundUser);
                                        context.SaveChanges();
                                        context.UpdatePasswordHashAndSalt(user.Name, 
                                                                          currentPasswordHash, currentPasswordSalt);
                                    }
                                    else
                                    {
                                        return ErrorResources.NoPermissionToUpdateUser;
                                    }
                                }
                                else
                                {
                                    return ErrorResources.NoPermissionToUpdateUser;
                                }
                            }
                            else
                            {   // the caller is updating someone else
                                // verify whether the caller is admin
                                if (IsAdminUser(GetUserByName(HttpContext.Current.User.Identity.Name)))
                                {
                                    // Re-generate password hash and password salt
                                    string currentPasswordSalt = HashHelper.CreateRandomSalt();
                                    string currentPasswordHash = HashHelper.ComputeSaltedHash(user.NewPassword, currentPasswordSalt);

                                    // requires the user to reset profile
                                    user.Settings.ProfileReset = 1;

                                    // saving changes
                                    context.Users.ApplyChanges(user);
                                    context.SaveChanges();
                                    context.UpdatePasswordHashAndSalt(user.Name, 
                                                                      currentPasswordHash, currentPasswordSalt);
                                }
                                else
                                {
                                    return ErrorResources.NoPermissionToUpdateUser;
                                }
                            }
                        }
                        else
                        {
                            // the call is from MyProfile screen
                            // and the caller can only update themselves
                            if (user.Name == HttpContext.Current.User.Identity.Name)
                            {
                                // Search user from database by name
                                User foundUser = context.Users.FirstOrDefault(u => u.Name == user.Name);

                                if (foundUser != null)
                                {
                                    // retrieve password salt
                                    string currentPasswordSalt = context.GetPasswordSalt(user.Name).First();
                                    string currentPasswordHash = context.GetPasswordHash(user.Name).First();
                                    // generate password hash
                                    string passwordHash = HashHelper.ComputeSaltedHash(user.Password, currentPasswordSalt);

                                    if (string.Equals(passwordHash, currentPasswordHash, StringComparison.Ordinal))
                                    {
                                        // Re-generate password hash and password salt
                                        currentPasswordSalt = HashHelper.CreateRandomSalt();
                                        currentPasswordHash = HashHelper.ComputeSaltedHash(user.NewPassword, currentPasswordSalt);

                                        // set the new password question
                                        foundUser.PasswordQuestion = user.PasswordQuestion;

                                        // re-generate passwordAnswer hash and passwordAnswer salt
                                        string currentPasswordAnswerSalt = HashHelper.CreateRandomSalt();
                                        string currentPasswordAnswerHash = HashHelper.ComputeSaltedHash(user.PasswordAnswer, currentPasswordAnswerSalt);

                                        foundUser.FullName.FirstName = user.FullName.FirstName;
                                        foundUser.FullName.LastName = user.FullName.LastName;
                                        foundUser.Email = user.Email;
                                        // no need to reset profile for the foundUser
                                        foundUser.Settings.ProfileReset = 0;

                                        // saving changes
                                        context.Users.ApplyChanges(foundUser);
                                        context.SaveChanges();
                                        context.UpdatePasswordHashAndSalt(user.Name, 
                                                                          currentPasswordHash, currentPasswordSalt);
                                        context.UpdatePasswordAnswerHashAndSalt(user.Name, 
                                                                                currentPasswordAnswerHash, currentPasswordAnswerSalt);
                                    }
                                    else
                                    {
                                        return ErrorResources.PasswordDoesNotMatch;
                                    }
                                }
                                else
                                {
                                    return ErrorResources.NoPermissionToUpdateUser;
                                }
                            }
                            else
                            {
                                return ErrorResources.NoPermissionToUpdateUser;
                            }
                        }
                        break;
                }
            }
            return string.Empty;
        }

        #region "Private Methods"

        private static User GetUserByName(string userName)
        {
            using (IssueVisionEntities context = new IssueVisionEntities())
            {
                return context.Users.FirstOrDefault(u => u.Name == userName);
            }
        }

        private static bool IsAdminUser(User user)
        {
            return (user.Settings.UserType == "A");
        }

        private static bool IssueIsReadOnly(Issue currentIssue)
        {
            // Admin user can read/update any issue
            if (IsAdminUser(GetUserByName(HttpContext.Current.User.Identity.Name)))
                return false;

            // normal user can only read/update issues assigned to them
            // or issues created by them and have not assigned to anyone.
            if (currentIssue.AssignedToID != null
                && currentIssue.AssignedToID == HttpContext.Current.User.Identity.Name)
                return false;
            if (currentIssue.AssignedToID == null
                && currentIssue.OpenedByID == HttpContext.Current.User.Identity.Name)
                return false;

            return true;
        }

        private static bool CheckUserInsertPermission(User user)
        {
            // cannot add itself
            if (user.Name == HttpContext.Current.User.Identity.Name)
                return false;

            // only admin user can insert a new user
            return IsAdminUser(GetUserByName(HttpContext.Current.User.Identity.Name));
        }

        private static bool CheckUserDeletePermission(User user)
        {
            // cannot delete itself
            if (user.Name == HttpContext.Current.User.Identity.Name)
                return false;

            // only admin user can delete a user
            return IsAdminUser(GetUserByName(HttpContext.Current.User.Identity.Name));
        }

        #endregion "Private Methods"
    }
}
